/**
 * Copyright (c) 2011 George 'kha0s' Zelenskiy <admin.kha0s@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * --------------------------------------------------------------------------------
 * Part:	Zee.SCGI.Server
 * Package: libzee-scgi
 * 
 * Implements simple, threaded, asynchronous, signal-based SCGI server interface.
 * 
 * To use it, create a Zee.SCGI.Server instance, providing working host, port
 * and maximum number of concurrent threads. Provide atleast one incoming
 * request handler by Zee.SCGI.Server.attach_request_handler() and start()
 * the server. 
 * 
 * On every new connection, after request body decomposition all request handlers
 * attached to handle_request signal will be called successively.
 *
 * Note that once attached request handler cannot be removed from handle_request
 * signal due to signal-delegate bug (?) in ValaC.
 */

namespace Zee.SCGI {
	/**
	 * Threaded asynchronous signal-based SCGI server
	 */
	public class Server {
		/**** Variables ****/
		/**
		 * Main loop to hold
		 */
		protected MainLoop 								_loop;
		/**
		 * Maximum numer of threads
		 */
		protected int 				 			 _max_threads;
		/**
		 * Server's internet address
		 */
		protected InetSocketAddress					 _address;
		/**
		 * Server's socket service
		 */
		protected ThreadedSocketService  			 _service;
		
		
		
		/**** Signals ****/
		/**
		 * Decomposed SCGI request handle signal
		 */
		protected signal void _handle_request_signal(Request req);
		
		
		
		/**** Delegates ****/
		/**
		 * Request handler's delegate
		 */
		public delegate void 	  RequestHandler(Request req);
		
		
		
		/**** Constructors ****/
		/**
		 * Initializes a new instance of Zee.SCGI.Server
		 *
		 * @param	address			string representing a server's IP
		 * @param	port			server's port to listen on
		 * @param	max_threads		maximum number of threads server will operate
		 */
		public Server(string address, uint16 port, int max_threads) {
			InetAddress addr = new InetAddress.from_string(address);
			
			this._address = new InetSocketAddress(addr, port);
			this._max_threads = max_threads;
		}
		
		
		
		/**** Properties ****/
		/**
		 * Internet address current instance is configured to work on
		 */
		public InetSocketAddress address {
			get { return this._address; }
		}
		
		/**
		 * Maximum number of threads
		 */
		public int max_threads {
			get { return this._max_threads; }
		}
		
		
		
		/**** Methods ****/
		/**
		 * Attaches a new method/function satisfying Zee.SCGI.Server.RequestHandler
		 * delegate to handle all incoming requests
		 *
		 * @param	handler			function to handle decomposed requests with
		 */
		public void attach_request_handler(RequestHandler handler) {
			//this._handle_request_signal.connect(handler);
			this._handle_request_signal.connect((req) => handler(req));
		}
		
		/**
		 * Starts SCGI server with current configuration
		 *
		 * @throws	Error			if specified internet address is not available or
		 *							cannot be configured GLib.Error will be thrown
		 */
		public void start() throws Error {
			this._service = new ThreadedSocketService(this._max_threads);
			
			// [TODO]: Wrap the generic GLib error
			this._service.add_address(this._address, SocketType.STREAM, SocketProtocol.TCP, null, null);
			
			this._service.incoming.connect(on_incoming_connection);
			this._service.start();
			
			(this._loop = new MainLoop(null, false)).run();
		}
		
		/**
		 * Asynchronously dispatches incoming connection
		 *
		 * @param	conn			socket connection associated with request
		 * @return					true in all cases
		 */
		protected bool on_incoming_connection(SocketConnection conn) {
			handle_request_async.begin(conn);
			return true;
		}
		
		/**
		 * Decomposes incoming SCGI request and calls handle_request signal subscribers
		 *
		 * @param	conn			socket connection associated with request
		 * @throws	IOError			if input stream is not available or request is invalid
		 *							GLib.IOError is thrown
		 */
		protected async void handle_request_async(SocketConnection conn) throws IOError {
			Request req = new Request();
			
			DataInputStream input = new DataInputStream(conn.input_stream);
			DataOutputStream output = new DataOutputStream(conn.output_stream);
			
			StringBuilder builder = new StringBuilder();
			uchar temp;
			
			try {
				while ((temp = input.read_byte(null)) != ':') {
					builder.append_c((char)temp);
				}
			
				int length = int.parse(builder.str);
				
				if (length > 0) {
					uchar[] buffer = new uchar[length];
					
					// [TODO]: May need to async-optimize the following
					
					if (input.read(buffer) == length) {
						req.input = input;
						req.output = output;
						
						string key = null;
						var temp_builder = new StringBuilder();
						
						for (int i = 0; i < length; i++) {
							if (buffer[i] == '\0') {
								if (key == null) {
									key = temp_builder.str;
								} else {
									req.params.insert(key, temp_builder.str);
									key = null;
								}
								
								temp_builder.erase(0, -1);
							} else {
								temp_builder.append_c((char)buffer[i]);
							}
						}
					}
				}
				
				input.read_byte(null);
			} catch (Error e) {
				throw new IOError.FAILED(e.message);
			}
			
			this._handle_request_signal(req);
		}
	}
}